﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using EnsureThat;

namespace LinqToStreams
{
	/// <summary>
	/// Extension methods.
	/// </summary>
	public static class Extends
	{
		/// <summary>
		/// Reads from the stream in a deferred fasion
		/// </summary>
		[DebuggerStepThrough]
		public static IEnumerable<byte> DeferredRead(this Stream stream)
		{
			Ensure.That(() => stream, Is.NotNull);
			return streamImpl(stream);
		}
		[DebuggerHidden]
		private static IEnumerable<byte> streamImpl(Stream stream)
		{
			while (true)
			{
				int i = stream.ReadByte();
				if (i == -1)
					break;
				yield return (byte)i;
			}
		}
		/// <summary>
		/// Gets chars from <paramref name="bytes"/> using the given <paramref name="encoding"/>.
		/// </summary>
		public static IEnumerable<char> Decode(this IEnumerable<byte> bytes, Encoding encoding)
		{
			Ensure.That(() => bytes, Is.NotNull);
			Ensure.That(() => encoding, Is.NotNull);
			using (var s = new EnumerableStream(bytes))
			{
				return DecodeBody(s, encoding);
			}
		}

		private static IEnumerable<char> DecodeBody(Stream s, Encoding encoding)
		{
			Decoder decoder = encoding.GetDecoder();

			// Guarantee the output buffer large enough to convert a few characters. 
			int UseBufferSize = 64;
			if (UseBufferSize < encoding.GetMaxCharCount(10))
				UseBufferSize = encoding.GetMaxCharCount(10);
			char[] chars = new char[UseBufferSize];

			// Intentionally make the input byte buffer larger than the output character buffer so the  
			// conversion loop executes more than one cycle.  

			byte[] bytes = new byte[UseBufferSize * 4];
			int bytesRead;
			do
			{
				// Read at most the number of bytes that will fit in the input buffer. The  
				// return value is the actual number of bytes read, or zero if no bytes remain. 

				bytesRead = s.Read(bytes, 0, UseBufferSize * 4);

				bool completed = false;
				int byteIndex = 0;
				int bytesUsed;
				int charsUsed;

				while (!completed)
				{
					// If this is the last input data, flush the decoder's internal buffer and state. 

					bool flush = (bytesRead == 0);
					decoder.Convert(bytes, byteIndex, bytesRead - byteIndex,
									chars, 0, UseBufferSize, flush,
									out bytesUsed, out charsUsed, out completed);

					// The conversion produced the number of characters indicated by charsUsed. Write that number 
					// of characters to the output file.

					for (int i = 0; i < charsUsed; i++)
						yield return chars[i];

					// Increment byteIndex to the next block of bytes in the input buffer, if any, to convert.
					byteIndex += bytesUsed;
				}
			}
			while (bytesRead != 0);
		}
		/// <summary>
		/// Encodes <paramref name="chars"/> into bytes using <paramref name="encoding"/>.
		/// </summary>
		public static IEnumerable<byte> Encode(this IEnumerable<char> chars, Encoding encoding)
		{
			Ensure.That(() => chars, Is.NotNull);
			Ensure.That(() => encoding, Is.NotNull);
			foreach (var c in chars)
				foreach (var b in encoding.GetBytes(new[] { c }))
					yield return b;
		}
		/// <summary>
		/// A stream wrapper around an enumerable.
		/// </summary>
		public class EnumerableStream : Stream
		{
			IEnumerator<byte> rator;
			#region Misc
			/// <summary>
			/// This stream can read.
			/// </summary>
			public override bool CanRead
			{
				get { return true; }
			}
			/// <summary>
			/// This stream cannot seek.
			/// </summary>
			public override bool CanSeek
			{
				get { return false; }
			}
			/// <summary>
			/// This stream cannot write.
			/// </summary>
			public override bool CanWrite
			{
				get { return false; }
			}
			/// <summary>
			/// Always throws NotSupportedException.
			/// </summary>
			public override void Flush()
			{
				throw new NotSupportedException();
			}
			/// <summary>
			/// Always throws NotSupportedException.
			/// </summary>
			public override long Length
			{
				get { throw new NotSupportedException(); }
			}
			/// <summary>
			/// Always throws NotSupportedException.
			/// </summary>
			public override long Position
			{
				get
				{
					throw new NotSupportedException();
				}
				set
				{
					throw new NotSupportedException();
				}
			}
			#endregion
			/// <summary>
			/// Reads from the enumerable.
			/// </summary>
			public override int Read(byte[] buffer, int offset, int count)
			{
				int j = 0;
				for (int i = offset; j < count && rator.MoveNext(); i++, j++)
				{
					buffer[i] = rator.Current;
				}
				return j;
			}

			#region More Misc
			/// <summary>
			/// Always throws NotSupportedException.
			/// </summary>
			public override long Seek(long offset, SeekOrigin origin)
			{
				throw new NotSupportedException();
			}
			/// <summary>
			/// Always throws NotSupportedException.
			/// </summary>
			public override void SetLength(long value)
			{
				throw new NotSupportedException();
			}
			/// <summary>
			/// Always throws NotSupportedException.
			/// </summary>
			public override void Write(byte[] buffer, int offset, int count)
			{
				throw new NotSupportedException();
			}
			/// <summary>
			/// Disposes the enumerator.
			/// </summary>
			protected override void Dispose(bool disposing)
			{
				base.Dispose(disposing);
				if (disposing)
					rator.Dispose();
			}
			/// <summary>
			/// Initializes the stream with a enumerable.
			/// </summary>
			public EnumerableStream(IEnumerable<byte> bytes)
			{
				if (bytes == null)
					throw new ArgumentNullException("bytes");
				var rator = bytes.GetEnumerator();
				if (rator == null)
					throw new ArgumentNullException("bytes.GetEnumerator()");
				this.rator = rator;
			}
			#endregion
		}
	}
}
